When writing code during the development of software applications, developers commonly spend a significant amount of time “debugging” the code to find runtime and other source code errors. In doing so, developers may take several approaches to reproduce and localize a source code bug, such as observing behavior of a program based on different inputs, inserting debugging code (e.g., to print variable values, to track branches of execution, etc.), temporarily removing code portions, etc. Tracking down runtime errors to pinpoint code bugs can occupy a significant portion of application development time.
Many types of debugging applications (“debuggers”) have been developed in order to assist developers with the code debugging process. These tools offer developers the ability to trace, visualize, and alter the execution of computer code. For example, debuggers may visualize the execution of code instructions, may present code variable values at various times during code execution, may enable developers to alter code execution paths, and/or may enable developers to set “breakpoints” and/or “watchpoints” on code elements of interest (which, when reached during execution, causes execution of the code to be suspended), among other things.
An emerging form of debugging applications enable “time travel,” “reverse,” or “historic” debugging. With “time travel” debugging, execution of a program (e.g., executable entities such as threads) is recorded/traced by a trace application into one or more trace files. These trace file(s) can then be used to replay execution of the program later, for both forward and backward analysis. For example, “time travel” debuggers can enable a developer to set forward breakpoints/watchpoints (like conventional debuggers) as well as reverse breakpoints/watchpoints.
During recording/tracing, a “time travel” debugger may take approaches to recording trace files that increase the practical amount of time that a program can be traced, that reduce the impact on the program being traced, and/or that reduce utilization of resources on the computer system(s) on which the traced program executes. For example, rather than storing a full record of memory addresses/values read and written during execution, some debuggers may record only the memory values that are consumed (read) by the program's code instructions. Additionally, rather than tracking each code instruction that was executed, some debuggers record only data relating to a small subset of these code instructions (e.g., the side effects of their execution, the register values supplied as inputs). Then, during replay, the programs' code is re-executed while being supplied with the traced memory and register values, which causes the program to re-execute in substantially the same manner that it did when it was traced—including reproducing the same memory state, processor register state, etc. at discrete points in time.
However, while the forgoing techniques can provide significant benefits during tracing (e.g., small trace files, low-overhead of tracing, etc.), the trace files they produce may not be optimally suited for a responsive debugging experience. For example, obtaining program state (e.g., memory and register values) at given points in time may involve replaying significant portions of program execution. This can provide an undesirable user experience, since it may take a debugger long periods of time to respond to a given user query (e.g., to replay to a given breakpoint and provide program state at that breakpoint).
For example, in order to respond to inquiries, existing time travel debuggers replay the entire trace in response to the inquiry. This involves the debugger single-stepping through each instruction, while keeping meticulous bookkeeping about the instructions executed. Then, the debugger uses this bookkeeping to produce a response to the inquiry. Thus, for example, if an existing time travel debugger were to receive an inquiry for functions that allocated memory, but did not later de-allocate that memory, the debugger would single-step through the entire trace in response to the inquiry, while keeping bookkeeping for each function called (e.g., their memory allocations and frees), and then use this bookkeeping information to form a response to the inquiry. The time needed for the debugger to replay the entire trace in response to a given inquiry can be substantial, often being on the order of hours to days for mere seconds of traced execution time.